// Copyright (c) Kurrent, Inc and/or licensed to Kurrent, Inc under one or more agreements.
// Kurrent, Inc licenses this file to you under the Kurrent License v1 (see LICENSE.md).

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

// for speed and simplicity this does all its generation based on the syntax tree and not on the semantic model
// there are essentially three phases:
// 1. use a SyntaxReceiver to detect and record uses of the attributes that we will generate code for
// 2. Register each usage, discovering how they are related to each other in the abstract syntax tree, bottom-up
// 3. Process the resulting relationship tree top-down, adding in the extra generated code that we want.
// todo: consider incremental generators if necessary. for now it looks like the generator is run
// only on build which could be sufficient for us
namespace KurrentDB.SourceGenerators.Messaging;

[Generator]
public class MessageSourceGenerator : ISourceGenerator {
	GeneratorExecutionContext _context;

	public void Initialize(GeneratorInitializationContext context) {
		context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
	}

	public void Execute(GeneratorExecutionContext context) {
		try {
			ExecuteImpl(context);
		} catch (Exception ex) {
			// tooling only shows the first line of the exception, so make the most of it
			var stack = ex.StackTrace.Replace("\r\n", " >> ");
			throw new Exception($"{ex.Message}. Stack: {stack}", ex);
		}
	}

	void ExecuteImpl(GeneratorExecutionContext context) {
		_context = context;

		if (context.SyntaxReceiver is not SyntaxReceiver syntaxReceiver) {
			throw new Exception("Unexpected syntax receiver");
		}

		var roots = new HashSet<CompilationUnitSyntax>();
		var members = new MemberTree();
		foreach (var candidate in syntaxReceiver.Candidates) {
			RegisterMember(candidate, members, roots);
		}

		foreach (var node in roots) {
			context.AddSource(
				GenFileName(node),
				SourceText.From(
					GenSource(node, members),
					Encoding.UTF8, SourceHashAlgorithm.Sha256));
		}
	}

	string GenSource(CompilationUnitSyntax node, MemberTree members) => $@"// autogenerated
using System.Threading;

#pragma warning disable CS0108 // Member hides inherited member; missing new keyword
{Transform(node, members).NormalizeWhitespace(indentation: "\t", eol: Environment.NewLine).ToFullString()}
";

	string GenFileName(SyntaxNode node) {
		var path = node.SyntaxTree.FilePath;
		var fileName = Path.GetFileNameWithoutExtension(path);
		return $"{fileName}-{(uint)path.GetHashCode()}.g.cs";
	}

	void RegisterMember(MemberDeclarationSyntax member, MemberTree members, HashSet<CompilationUnitSyntax> roots) {
		if (members.TryGetValue(member.Parent, out var siblings)) {
			siblings.Add(member);
		} else {
			members[member.Parent] = new HashSet<MemberDeclarationSyntax> { member };
			if (member.Parent is MemberDeclarationSyntax mds) {
				RegisterMember(mds, members, roots);
			} else if (member.Parent is CompilationUnitSyntax cus) {
				roots.Add(cus);
			} else {
				throw new Exception($"Unexpected parent: \"{member.Parent.Kind()}\"");
			}
		}
	}

	CompilationUnitSyntax Transform(CompilationUnitSyntax node, MemberTree members) =>
		SyntaxFactory
			.CompilationUnit()
			.AddMembers(TransformChildren(node, members));

	MemberDeclarationSyntax[] TransformChildren(SyntaxNode node, MemberTree members) =>
		members.TryGetValue(node, out var children)
			? children
				.Select(x => Transform(x, members))
				.ToArray()
			: Array.Empty<MemberDeclarationSyntax>();

	MemberDeclarationSyntax Transform(MemberDeclarationSyntax node, MemberTree members) =>
		node switch {
			NamespaceDeclarationSyntax x => Transform(x, members),
			FileScopedNamespaceDeclarationSyntax x => Transform(x, members),
			ClassDeclarationSyntax x => Transform(x, members),
			_ => throw new Exception($"Unexpected MemberDeclarationSyntax syntax: \"{node.Kind()}\"")
		};

	NamespaceDeclarationSyntax Transform(NamespaceDeclarationSyntax node, MemberTree members) =>
		SyntaxFactory
			.NamespaceDeclaration(
				name: node.Name,
				externs: default,
				usings: node.Usings,
				members: default)
			.AddMembers(TransformChildren(node, members));

	FileScopedNamespaceDeclarationSyntax Transform(FileScopedNamespaceDeclarationSyntax node, MemberTree members) =>
		SyntaxFactory
			.FileScopedNamespaceDeclaration(
				attributeLists: default,
				modifiers: default,
				name: node.Name,
				externs: default,
				usings: node.Usings,
				members: default)
			.AddMembers(TransformChildren(node, members));

	ClassDeclarationSyntax Transform(ClassDeclarationSyntax node, MemberTree members) {
		if (!node.Modifiers.Any(SyntaxKind.PartialKeyword))
			_context.ReportImpartialMessage(node);

		return SyntaxFactory
			.ClassDeclaration(
				attributeLists: default,
				modifiers: node.Modifiers,
				identifier: node.Identifier,
				typeParameterList: node.TypeParameterList,
				baseList: default,
				constraintClauses: default,
				members: default)
			.AddMembers(TransformChildren(node, members))
			.AddGeneratedMembers(_context, node)
			.WithoutTrivia();
	}

	class SyntaxReceiver : ISyntaxReceiver {
		public List<MemberDeclarationSyntax> Candidates { get; } = new();

		public void OnVisitSyntaxNode(SyntaxNode syntaxNode) {
			if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax &&
				(classDeclarationSyntax.TryGetDerivedMessageAttribute(out _) ||
				 classDeclarationSyntax.TryGetBaseMessageAttribute(out _))) {

				Candidates.Add(classDeclarationSyntax);
			}
		}
	}

	class MemberTree : Dictionary<SyntaxNode, HashSet<MemberDeclarationSyntax>> {
	}
}